Adding Uncertainty Penalty

This tutorial will guide you on how to add the uncertainty penalty to the PortfolioOptimizer classes. As of writing the two optimizer class that supports penalty functions are PortfolioOptimizer and ActivePortfolioOptimizer.

To start off, let’s load in all the required packages.

[1]:
from muarch.funcs import get_annualized_sd

from allopy import OptData, PortfolioOptimizer
from allopy.datasets import load_monte_carlo
from allopy.penalty import UncertaintyPenalty

import numpy as np


np.set_printoptions(linewidth=200)

Let’s load in a sample dataset.

[2]:
data = OptData(load_monte_carlo(), 'q')
data.shape
[2]:
(80, 10000, 9)

We’ll only use the first 7 asset classes. For them we will also set the lower and upper bounds respectively.

[3]:
opt = PortfolioOptimizer(data.take_assets(7))
opt.set_bounds(
    0,  # lower bounds all set  to 0
    [0.4, 0.3, 0.13, 0.11, 0.25, 0.04, 0.05]  # custom upper bounds
)
[3]:
<allopy.optimize.portfolio.portfolio.optimizer.PortfolioOptimizer at 0x263fc627208>

For simplicity, we will use the current volatility as the uncertainty vector. But remember, you can set a uncertainty matrix for the penalty class.

[4]:
vol = get_annualized_sd(data, 'quarter')
vol.round(4)
[4]:
array([0.1849, 0.2648, 0.2026, 0.0961, 0.078 , 0.1403, 0.0428, 0.0613, 0.185 ])
[5]:
penalty = UncertaintyPenalty(vol, lambda_=1.0)
print(penalty)
UncertaintyPenalty(
    lambda=1.0,
    uncertainty=[[0.1849, 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.2648, 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.2026, 0.    , 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.0961, 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.078 , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.1403, 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.0428, 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.0613, 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.185 ]],
    method=direct
)

Note that we have transformed the vector to a diagonal matrix. Now, let’s add the penalty to the optimizer.

[6]:
try:
    opt.penalty = penalty
except AssertionError as e:
    print("An error has occurred:", '. '.join(e.args))
An error has occurred: dimension of the penalty does not match the data

Oops, why is there an error? It’s because the penalty vector has a dimension of 9, which means that there should be 9 asset classes. However, at the top, we have chosen to use only the first 7 asset classes. To fix that, we must initialize the UncertaintyPenalty correctly. Let’s do it again.

[7]:
penalty = UncertaintyPenalty(vol[:7], lambda_=1.0)
print(penalty)
UncertaintyPenalty(
    lambda=1.0,
    uncertainty=[[0.1849, 0.    , 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.2648, 0.    , 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.2026, 0.    , 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.0961, 0.    , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.078 , 0.    , 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.1403, 0.    ],
       [0.    , 0.    , 0.    , 0.    , 0.    , 0.    , 0.0428]],
    method=direct
)
[8]:
opt.penalty = penalty
optimal_weights = opt.maximize_sharpe_ratio()
optimal_weights.round(4)
[8]:
array([0.2678, 0.1522, 0.13  , 0.11  , 0.25  , 0.04  , 0.05  ])

That’s it, we’re done. To remove any penalty you have set by accident, set it to None.

[9]:
opt.penalty = None